Skip to content

Drop clap in favour of a hand-rolled argument parser#13

Open
Mic92 wants to merge 1 commit intoLordGrimmauld:mainfrom
Mic92:no-clap
Open

Drop clap in favour of a hand-rolled argument parser#13
Mic92 wants to merge 1 commit intoLordGrimmauld:mainfrom
Mic92:no-clap

Conversation

@Mic92
Copy link
Copy Markdown

@Mic92 Mic92 commented Apr 12, 2026

This shim is ~90 lines that assemble an argv for run0 and exec() it. Pulling in clap with the derive feature for that brought 21 transitive crates (syn, quote, proc-macro2, anstream, windows-sys, ...) totalling roughly 460k lines of vendored Rust into the build of something that may sit on the system's sudo path. That is a lot of supply-chain surface, lockfile churn and build time for what is, in the end, one getopt loop over a fixed option set defined by sudo(8).

This is not a theoretical concern. The xz/liblzma backdoor (CVE-2024-3094) reached sshd not because OpenSSH was careless but because a transitive dependency two hops away was compromised; a program in sudo's seat is exactly the kind of target that makes deep dependency trees worth attacking. The Rust ecosystem has had its own reminders too: the serde_derive precompiled-binary episode showed that even ubiquitous, trusted proc-macro crates can quietly start running opaque code at build time, and typosquats like rustdecimal have shipped malware through crates.io. Every crate in Cargo.lock is a crate someone has to keep watching.

Replace it with a small table-driven parser: a single (short, long, takes_value) table feeds one apply() dispatch, so short and long forms share the same code path. It supports exactly what the shim used from clap (clustered shorts, attached and separate values, --opt=val, --, counted -l, repeatable --run0-extra-arg, --preserve-env[=LIST] with require_equals semantics, and unknown options falling through to the command) and is covered by unit tests for each of those edges. Ignored sudo flags are still parsed so they consume their arguments correctly, they just no longer get struct fields.

The net result is zero dependencies, the release binary roughly halved (1.2M -> 570K), cold release builds down from ~10s to ~1s, and 21 fewer crates to review on every cargo update, at the cost of about 200 lines of straight-line, unsafe-free code we own and test ourselves.

This shim is ~90 lines that assemble an argv for run0 and exec() it.
Pulling in clap with the derive feature for that brought 21 transitive
crates (syn, quote, proc-macro2, anstream, windows-sys, ...) totalling
roughly 460k lines of vendored Rust into the build of something that
may sit on the system's sudo path. That is a lot of supply-chain
surface, lockfile churn and build time for what is, in the end, one
getopt loop over a fixed option set defined by sudo(8).

This is not a theoretical concern. The xz/liblzma backdoor
(CVE-2024-3094) reached sshd not because OpenSSH was careless but
because a transitive dependency two hops away was compromised; a
program in sudo's seat is exactly the kind of target that makes deep
dependency trees worth attacking. The Rust ecosystem has had its own
reminders too: the serde_derive precompiled-binary episode showed that
even ubiquitous, trusted proc-macro crates can quietly start running
opaque code at build time, and typosquats like rustdecimal have shipped
malware through crates.io. Every crate in Cargo.lock is a crate someone
has to keep watching.

Replace it with a small table-driven parser: a single (short, long,
takes_value) table feeds one apply() dispatch, so short and long forms
share the same code path. It supports exactly what the shim used from
clap (clustered shorts, attached and separate values, --opt=val, --,
counted -l, repeatable --run0-extra-arg, --preserve-env[=LIST] with
require_equals semantics, and unknown options falling through to the
command) and is covered by unit tests for each of those edges. Ignored
sudo flags are still parsed so they consume their arguments correctly,
they just no longer get struct fields.

The net result is zero dependencies, the release binary roughly halved
(1.2M -> 570K), cold release builds down from ~10s to ~1s, and 21 fewer
crates to review on every cargo update, at the cost of about 200 lines
of straight-line, unsafe-free code we own and test ourselves.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant